在解釋非同步(Asynchronous)之前,我們先來聊聊什麼是同步(synchronous)
首先我們需要先知道一件事情是:Dart 是一個單執行緒(single thread)語言,也就是一次只做一件事情,而我們的程式在沒有非同步事件之前,我們的程式碼會逐行執行。而這也就是同步的概念:所有程式都是依序執行,上一個事情做完了我才開始做下一個。
但你也許會想問如果我其中一個事情要做特別久呢?想像一下如果我們程式碼全部都是同步的話,我們有一個按鈕按下去後會向後端伺服器取得資料然後到前端渲染。
大致流程會像這樣:按鈕點擊 → fetch api → 前端重新渲染,假設api server 要兩秒後才會吐回資料,在沒有非同步的概念下他是會依序執行的,那就會發生我按下按鈕後:
在這期間主執行緒是被佔滿的導致無法進行任何操作, 我的畫面會直接停頓兩秒 ,這樣子就能體會非同步有多重要了吧。
但如果 fetch api 這裡是一個非同步的行為,我們這兩秒就可以繼續滑動畫面按其他按鈕之類之類,然後兩秒後api依然會傳資料然後畫面重新渲染。
簡單來說 isolate
就是 Dart 程式碼的執行環境,它擁有自己event Loop及記憶體。
我們的 main function 開始執行時也就會先創立一個main isolate
而之後所有的程式碼都會在這個isolate
裡執行。
雖然在Dart裡我們可以自己創建 isolate
,但其實大多數狀況我們都只需要一個isolate
(也就是main isolate
),直到某個運算真的大到會讓我們發生卡頓、掉幀時,我們才需要來考慮使用isolate
。
因為每個 isolate
就如同他的名字每個都是被隔離的環境,所以我們必須用其他方式來與它進行互動,isolate
間只能透過port交換訊息。
import 'dart:isolate';
void longtimeTask() {
for (var i = 0; i < 10000000000; i++) {}
return null;
}
void foo(SendPort sendPort) {
// 即使有這longtimeTask 也不會阻塞住執行緒
// 外面的isolateDemo end 馬上就會被print
longtimeTask();
sendPort.send("foo");
}
void isolateDemo() async {
print('isolateDemo start');
final receivePort = ReceivePort();
final isolate = await Isolate.spawn<SendPort>(foo, receivePort.sendPort);
receivePort.listen((data) {
print('isolate:$data');
receivePort.close();
isolate.kill(priority: Isolate.immediate);
});
// 將這行打開執行緒會被佔據
// longtimeTask();
print('isolateDemo end');
}
先宣告一個 ReceivePort
然後 Isolate.spawn
將要執行的function及port傳進去,然後在用 receivePort.listen
讀取 Isolate
有無訊息傳出來。
這邊會看到我們的 foo
裡有一個 sendPort.send
就是將訊息傳出去的方法。而我們這邊也放了一個 longtimeTask
來模擬這個Isolate
執行了一個很久的運算。
如果讀者實際運行程式碼會發現輸出的順序是
isolateDemo start
isolateDemo end
//這篇會停頓兩三秒後在輸出下面那行
isolate: foo
這時候就能發現 Isolate
將運算隔離出去了,並沒有阻塞住執行緒。
如果將註解裡的 longtimeTask
打開會發現
isolateDemo start
//這篇會停頓兩三秒後在輸出下面兩行
isolateDemo end
isolate: foo
longtimeTask
將執行去阻塞住導致 isolateDemo end
這邊要停頓後才會輸出,但也許有人會想那為什麼 foo
這邊沒有再停頓一次而是直接跟著輸出呢?因為這個Isolate
跟我們main Isolate
是隔開的所以他已經在背景運算完並 「通知執行緒已經完成接著等著執行緒執行他的運算結果」 ,但這件事情是如何做到的,就跟接下來要介紹的 Event loops 有十分密切的關係。
以網頁、App來說這種需要與使用者互動的程式來說,從開始執行到結束的這段時間中,程式本身不會預先知道自己在什麼時間點會被怎樣操作,所以這類程式通常會設計會執行在一個 「永不阻塞的單執行緒」(single-threaded & non-blocking) ,然後再利用Event loops的消化這些事件來。
Dart 裡的 Event loops 與 JS 相似,有兩個 FIFO 的 queue 分別為event queue及 microtask queue:
isolate
互相溝通的事件等等先來看一下 Dart Event loops 的流程圖
從上面這張流程圖可以得知Dart是這樣執行event loop的:
main()
那這樣子到底是如何讓單一個isolate
(或者可以說執行緒)達成非同步的呢?就如同我們前面說到 「程式本身不會預先知道自己在什麼時間點會被怎樣操作」 ,意味著我們的執行緒大多數時候其實都是在 「等待操作」 的,根本原因是因為這是一種被稱為 「非阻塞式呼叫」 的概念,當執行這類呼叫時我們的執行緒並不會被佔據,直到這個 「非阻塞式呼叫」 的有了結果才會佔用到執行緒。
我們可以先來看一下這個簡單的範例:
Timer(Duration(seconds: 0), () {
print('Timer 1');
});
print('normal print 1 ');
這邊有一個timer設定為0秒後會執行 print('Timer 1')
及一個一般的 print('normal print 1 ')
這邊讀者可以先來想想執行結果會是
Timer 1
normal print 1
還是
normal print 1
Timer 1
相信有寫過JS的讀者應該已經知道答案了。
就是會先輸出 normal print 1
之後才是 Timer 1
,也許有人會有疑問「Timer 0秒不就會是馬上執行嗎?」
回想一下上面提到的 「非阻塞式呼叫」 ,我們會等到他有結果時,他才會執行雖然是0秒但他是 「0秒後放到執行緒」精確來說是「0秒後放進event queue」,又因為event queue是FIFO所以 normal print 1
就會先被執行到了。
當然有 「非阻塞式呼叫」 就有 「阻塞式呼叫」 ,與 「非阻塞式呼叫」 相反就是在有結果之前都會一直佔據著執行緒(Hang 住)。
稍微來整理一下現在提到了幾個抽象概念
同步與非同步
同步:執行緒執行完一件事之前不能做其他事情,必須一件一件接著做。
非同步:執行緒執行一件事情在沒有得到結果前可以先去做其他事情,之後再回來做。
阻塞與非阻塞
阻塞:執行緒執行時會被Hang住。
非阻塞:執行緒不會被Hang住,但之後會去查詢有無結果返回。
所以同步與非同步指的是 「我們使用者與執行緒」的關係,使用者有無必要等結果返回,而阻塞與非阻塞關注的是「執行緒本身的狀態」 它自己需不需要等待結果回傳,還是要先去做其他事情。
我們現在知道了Dart裡有非同步/同步之分,知道了他的好處但感覺就是有點難控制,畢竟我們不知道他何時會回傳要是我有事情要等前面的非同步做完才想接著做那我該怎麼辦?
其實在 Timer
的例子裡就有一個控制非同步的手法叫做 callback
,意思是當這個非同步有了結果後就執行這個 callback function
所以如果,我有好幾個要依照順序做的非同步,我們就會有一個非常巢狀的callback,也就是早期JS所說的 「 callback hell」
當然Dart也有提供一些方法讓我們更方便的控制非同步,明天我們就要開始進入如何簡單的控制這些非同步操作。
今天的程式碼也有放到github上
https://github.com/zxc469469/dart-playground/tree/Day08/event-loop